package cn.kevinkun.Box2D;

import java.util.ArrayList;
import java.util.List;

import org.jbox2d.callbacks.ContactImpulse;
import org.jbox2d.callbacks.ContactListener;
import org.jbox2d.callbacks.QueryCallback;
import org.jbox2d.collision.AABB;
import org.jbox2d.collision.Manifold;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.Fixture;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.contacts.Contact;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesAssets;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.Component;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.EventDispatcher;
import com.google.appinventor.components.runtime.HVArrangement;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

@DesignerComponent(version = 1, //
		versionName = "20240315", //
		category = ComponentCategory.EXTENSION, //
		description = "A Java Physics Engine based on Jbox2d. For more information of JBox2d, you can check here: http://www.jbox2d.org/", //
		iconName = "aiwebres/logo.jpg", //
		nonVisible = true, //
		helpUrl = "https://kevinkun.cn/box2d")
@UsesAssets(fileNames = "")
@UsesLibraries(libraries = "jbox2d-library-2.2.1.1.jar")
@SimpleObject(external = true)

public class BoxWorld extends AndroidNonvisibleComponent implements Component {

	private Context context;

	private BoxView boxView;
	private World world;

	public List<Body> bodyToDestroy = new ArrayList<>();

	private float dt = 1f / 30f;
	private int width;
	private int height;
	private int velocityIterations = 3;
	private int positionIterations = 10;

	private float gravityX = 0f;
	private float gravityY = 0f;

	public BoxWorld(ComponentContainer container) {
		super(container.$form());

		this.context = container.$context();

	}

	@SimpleFunction(description = "")
	public void CreateWorld(HVArrangement layout) {
		ViewGroup vGroup = (ViewGroup) layout.getView();
		boxView = new BoxView(context);
		vGroup.addView(boxView, -1, -1);

	}

	@SimpleEvent(description = "body param contains the Body which is touched, or empty String if no Body is touched")
	public void TouchDown(float x, float y, Object body) {
		EventDispatcher.dispatchEvent(this, "TouchDown", x, y, body);
	}

	@SimpleEvent(description = "body param contains the Body which is touched, or empty String if no Body is touched")
	public void TouchUp(float x, float y, Object body) {
		EventDispatcher.dispatchEvent(this, "TouchUp", x, y, body);
	}

	@SimpleEvent(description = "body param contains the Body which is touched, or empty String if no Body is touched")
	public void Dragged(float startX, float startY, float lastX, float lastY, float x, float y, Object body) {
		EventDispatcher.dispatchEvent(this, "Dragged", startX, startY, lastX, lastY, x, y, body);
	}

	@SimpleEvent
	public void BeginContact(Body body1, Body body2) {
		EventDispatcher.dispatchEvent(this, "BeginContact", body1, body2);
	}

	@SimpleEvent
	public void EndContact(Body body1, Body body2) {
		EventDispatcher.dispatchEvent(this, "EndContact", body1, body2);
	}

	@SimpleEvent
	public void Step() {
		EventDispatcher.dispatchEvent(this, "Step");
	}

//	@SimpleEvent
//	public void PreSolve(Body body1, Body body2) {
//		EventDispatcher.dispatchEvent(this, "PreSolve", body1, body2);
//	}
//
//	@SimpleEvent
//	public void PostSolve(Body body1, Body body2) {
//		EventDispatcher.dispatchEvent(this, "PostSolve", body1, body2);
//	}

	@SimpleEvent
	public void WorldCreated() {
		EventDispatcher.dispatchEvent(this, "WorldCreated");
	}

	@SimpleProperty(description = "gravity in X direction")
	public void GravityX(float gx) {
		gravityX = gx;
		if (world != null) {
			world.setGravity(new Vec2(gravityX, gravityY));
		}
	}

	@SimpleProperty(description = "how many steps in one second, normally 30-60. default to 60")
	public void TimeStep(int step) {
		dt = 1f / step;
	}

	@SimpleProperty(description = "gravity in Y direction")
	public void GravityY(float gy) {
		gravityY = gy;
		if (world != null) {
			world.setGravity(new Vec2(gravityX, gravityY));
		}
	}

	@SimpleProperty(description = "world width in pixel")
	public float WorldWidth() {
		return width;
	}

	@SimpleProperty(description = "world height in pixel")
	public float WorldHeight() {
		return height;
	}

	public void createWorld() {
		if (world == null) {
			world = new World(new Vec2(gravityX, gravityY));

			world.setAllowSleep(true);

			world.setContactListener(new ContactListener() {

				@Override
				public void preSolve(Contact contact, Manifold oldManifold) {
					// TODO Auto-generated method stub
					// PreSolve(contact.getFixtureA().getBody(), contact.getFixtureB().getBody());
				}

				@Override
				public void postSolve(Contact contact, ContactImpulse impulse) {
					// TODO Auto-generated method stub
					// PostSolve(contact.getFixtureA().getBody(), contact.getFixtureB().getBody());
				}

				@Override
				public void endContact(Contact contact) {
					EndContact(contact.getFixtureA().getBody(), contact.getFixtureB().getBody());

				}

				@Override
				public void beginContact(Contact contact) {
					BeginContact(contact.getFixtureA().getBody(), contact.getFixtureB().getBody());

				}
			});
			WorldCreated();
		}
	}

	public void step() {

		world.step(dt, velocityIterations, positionIterations);
		for (Body body : bodyToDestroy) {
			ImageView imageView = (ImageView) body.getUserData();
			world.destroyBody(body);
			body = null;
			boxView.removeView(imageView);
		}
		bodyToDestroy = new ArrayList<>();
		int childCount = boxView.getChildCount();
		for (int i = 0; i < childCount; i++) {
			View view = boxView.getChildAt(i);
			Body body = (Body) view.getTag();
			if (body != null) {
				view.setX(BoxUtil.M2P(body.getPosition().x) - view.getWidth() / 2);
				view.setY(BoxUtil.M2P(body.getPosition().y) - view.getHeight() / 2);
				view.setRotation(BoxUtil.radiansToDegrees(body.getAngle() % 360));
			}
		}
		boxView.invalidate();
		Step();
	}

	public ImageView addImageView(int left, int top, int width, int height, String image) {
		FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
				FrameLayout.LayoutParams.WRAP_CONTENT);
		layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
		layoutParams.setMargins(left, top, 0, 0);
		layoutParams.height = height;
		layoutParams.width = width;

		ImageView imageView = new ImageView(context);
		imageView.setScaleType(ScaleType.FIT_XY);
		Bitmap bm = BitmapFactory.decodeFile(BoxUtil.getAbsolutePath(form, image));
		imageView.setImageBitmap(bm);

		boxView.addView(imageView, layoutParams);

		return imageView;
	}

	private class BoxView extends FrameLayout {

		private float startX = -1;
		private float startY = -1;
		private float lastX = -1;
		private float lastY = -1;
		private boolean isDrag = false;
		private boolean drag = false;
		private int tapThreshold = 10;

		private BoxView(@NonNull Context context) {
			this(context, null);

		}

		private BoxView(@NonNull Context context, @Nullable AttributeSet attrs) {
			super(context, attrs);

			setWillNotDraw(false);
			// createWorld();

		}

		@Override
		protected void onSizeChanged(int w, int h, int oldw, int oldh) {
			super.onSizeChanged(w, h, oldw, oldh);
			width = w;
			height = h;

		}

		@Override
		protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
			super.onLayout(changed, left, top, right, bottom);
			createWorld();

		}

		@Override
		protected void onDraw(Canvas canvas) {
			super.onDraw(canvas);
			step();

		}

		@Override
		public boolean onTouchEvent(MotionEvent event) {
			float x = event.getX();
			float y = event.getY();
			AABB aabb = new AABB();
			aabb.lowerBound.set(BoxUtil.pixelToVec2(x - 0.001f, y - 0.001f));
			aabb.upperBound.set(BoxUtil.pixelToVec2(x + 0.001f, y + 0.001f));

			TestQueryCallback cb = new TestQueryCallback(BoxUtil.pixelToVec2(x, y));
			world.queryAABB(cb, aabb);
			Body body = null;
			if (cb.fixture != null) {
				body = cb.fixture.getBody();
			}
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN: {
				startX = x;
				startY = y;
				lastX = x;
				lastY = y;
				drag = false;
				isDrag = false;
				if (body != null) {
					TouchDown(x, y, body);
				} else {
					TouchDown(x, y, "");

				}

				break;
			}
			case MotionEvent.ACTION_MOVE: {
				if (!isDrag && (Math.abs(x - startX) < tapThreshold && Math.abs(y - startY) < tapThreshold)) {
					break;
				}
				isDrag = false;
				drag = true;
				if (body != null) {
					Dragged(startX, startY, lastX, lastY, x, y, body);
				} else {
					Dragged(startX, startY, lastX, lastY, x, y, "");
				}

				lastX = x;
				lastY = y;
				break;
			}

			case MotionEvent.ACTION_UP: {

				if (body != null) {
					TouchUp(x, y, body);
				} else {
					TouchUp(x, y, "");
				}

				drag = false;
				startX = -1;
				startY = -1;
				lastX = -1;
				lastY = -1;
				break;
			}

			}
			return true;
		}

	}

	class TestQueryCallback implements QueryCallback {
		public final Vec2 point;
		public Fixture fixture;

		public TestQueryCallback(Vec2 vec) {
			point = vec;
			fixture = null;
		}

		@Override
		public boolean reportFixture(Fixture argFixture) {
			Body body = argFixture.getBody();
			// if (body.getType() == BodyType.DYNAMIC) {
			boolean inside = argFixture.testPoint(point);
			if (inside) {
				fixture = argFixture;
				// BodyTouchDown(body);
				return false;
			}
			// }

			return true;
		}
	}

	public World getWorld() {
		return world;
	}

}
